Skip to content

[TrimmableTypeMap] Fix stale legacy JCW contamination when switching typemap flavors#11098

Draft
simonrozsival wants to merge 3 commits intomainfrom
dev/simonrozsival/fix-trimmable-register-guard
Draft

[TrimmableTypeMap] Fix stale legacy JCW contamination when switching typemap flavors#11098
simonrozsival wants to merge 3 commits intomainfrom
dev/simonrozsival/fix-trimmable-register-guard

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented Apr 9, 2026

Problem

When switching _AndroidTypeMapImplementation between llvm-ir and trimmable without a clean build, stale legacy JCWs contaminate the trimmable APK.

What happens

  1. Build with llvm-ir: GenerateJavaCallableWrappers writes JCWs to android/src/ with Runtime.register(typeName, klass, methods) calls.
  2. Build with trimmable: GenerateTrimmableTypeMap emits JCWs that should only call Runtime.registerNatives(klass), then places them in android/src/.
  3. JCWs with different CRC-based package names survive the overlay, so both old and new wrappers can coexist.
  4. _FindJavaStubFiles globs all *.java from android/src/, including stale legacy JCWs.
  5. Both sets get compiled into classes.dex.
  6. At runtime, a stale legacy JCW can call Runtime.register(), triggering the reflection-based registration path that is incompatible with trimming.

Previously, the C++ side silently dropped Runtime.register() calls because registerJniNativesFn was nullptr for trimmable mode, which masked the bug.

Fix (three layers)

1. Write JCWs directly to _AndroidIntermediateJavaSourceDirectory

The trimmable generator now writes to _AndroidIntermediateJavaSourceDirectory (android/src/) directly instead of typemap/java/ + copy. This removes the extra overlay step where stale files could survive.

2. Add _AndroidTypeMapImplementation to the properties cache

Switching typemap implementations now triggers _CleanIntermediateIfNeeded, which clears stale intermediate artifacts including android/src/.

3. Always wire registerJniNativesFn

Instead of silently dropping Runtime.register() calls in C++, the function pointer is always set. If a stale JCW somehow still calls Runtime.register(), the call now flows through to C# where TrimmableTypeMapTypeManager.RegisterNativeMembers throws UnreachableException.

Tests

  • Unit test (JcwJavaSourceGeneratorTests): verifies trimmable JCWs emit Runtime.registerNatives (klass) and never emit the legacy Runtime.register(" path.
  • Build integration test (BuildTest.SwitchingTypeMapImplementationTriggersClean): verifies that switching _AndroidTypeMapImplementation between llvm-ir and trimmable triggers _CleanIntermediateIfNeeded, while a no-change rebuild skips it.

Files changed

  • JNIEnvInit.cs — remove the if (!TrimmableTypeMap) guard on registerJniNativesFn
  • Trimmable.targets — write JCWs to _AndroidIntermediateJavaSourceDirectory directly and remove the extra output/copy step
  • Common.targets — add _AndroidTypeMapImplementation to _PropertyCacheItems
  • JcwJavaSourceGeneratorTests.cs — regression coverage for positive registerNatives and negative Runtime.register assertions
  • BuildTest.cs — integration test for typemap implementation switching

@simonrozsival simonrozsival force-pushed the dev/simonrozsival/fix-trimmable-register-guard branch from de004d7 to 36b07f1 Compare April 9, 2026 08:43
@simonrozsival simonrozsival changed the title [TrimmableTypeMap] Always wire registerJniNativesFn, fail loudly if trimmable JCW calls Runtime.register() [TrimmableTypeMap] Fix stale legacy JCW contamination when switching typemap flavors Apr 9, 2026
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/fix-trimmable-register-guard branch from 36b07f1 to e628b09 Compare April 9, 2026 08:51
…typemap flavors

When switching MonoAndroidTypeMapFlavor between legacy and trimmable
without a clean build, stale legacy JCWs with Runtime.register(typeName,
klass, methods) calls persisted in the android/src/ intermediate
directory alongside trimmable JCWs that only use Runtime.registerNatives().
Both got compiled into the APK, causing the legacy registration path to
execute at runtime — a path incompatible with trimming.

Root cause: the trimmable JCW generator wrote to typemap/java/ and then
copied to android/src/, but never cleaned android/src/ first. Legacy
JCWs with different package names survived the copy overlay.

Three fixes:

1. Write trimmable JCWs directly to _AndroidIntermediateJavaSourceDirectory
   (android/src/) instead of a separate typemap/java/ directory, eliminating
   the copy step entirely. This is the same directory that _FindJavaStubFiles
   globs from, so no files can be missed or stale.

2. Add MonoAndroidTypeMapFlavor to the build properties cache so switching
   between legacy and trimmable triggers _CleanIntermediateIfNeeded,
   removing all stale artifacts from the previous flavor.

3. Always wire registerJniNativesFn so that if a stale JCW somehow calls
   Runtime.register(), TrimmableTypeMapTypeManager.RegisterNativeMembers
   throws UnreachableException with a clear diagnostic instead of the C++
   side silently dropping the call.

Also adds a unit test verifying trimmable JCWs never emit Runtime.register().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/fix-trimmable-register-guard branch from e628b09 to d3cfd10 Compare April 9, 2026 09:08
@simonrozsival simonrozsival added trimmable-type-map copilot `copilot-cli` or other AIs were used to author this labels Apr 9, 2026
simonrozsival and others added 2 commits April 9, 2026 17:44
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant